#include <video-graphics.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <codepage.h>
#include <utypes.h>

#include "window.h"
#include "heap.h"

struct Window_ {
	int x; /**< Posição horizontal da janela na tela */
	int y; /**< Posição vertical da janela na tela */
	unsigned int w; /**< Largura da janela */
	unsigned int h; /**< Altura da janela */
	/** Callback que desenha a janela 
	 */
	WindowDrawFunc draw;
	/** Função usada para modificar o estado interno da janela
	 */
	WindowUpdateFunc update;
	/** Função que libera os recursos usados pelos dados
	 */
	void (*free) (void *data);

	void *data; /**< Dados passados para as callbacks */
	int plane; /**< Plano da janela */
};

/** Heap de janelas
 */
static Heap win_heap;

/** Heap auxiliar
 */
static Heap aux_heap;

/** Próximo plano
 */
static unsigned int next_plane;

/** Memória gráfica
 */
static char *base;

/** Buffer de double-buffering
 */
static char *dbuffer;

/** Mapeamento
 */
__dpmi_meminfo map;

/** Coordenada x do sistemas de janela que corresponde ao canto superior esquerdo do ecrã
 */
int winsys_x;

/** Coordenada y do sistemas de janela que corresponde ao canto superior esquerdo do ecrã
 */
int winsys_y;

/** Tabela de caracteres
 */
char char_table[256*CHR_H];

/** Função de comparação de janelas usada para as ordenar no heap
 */
static int window_cmp (void *w1, void *w2) 
{
	Window win1 = (Window) w1;
	Window win2 = (Window) w2;
	
	return (win1->plane - win2->plane);
}

/**
 * \todo Inicializar o rato
 */
void winsys_init (enum Resolution res) 
{
	//Entra no modo gráfico
	winsys_graphicmode (res);
	// Inicializa globais
	winsys_x = 0;
	winsys_y = 0;
	next_plane = 0;
	win_heap = heap_new (window_cmp);
	aux_heap = heap_new (window_cmp);
	// Carrega a tabela de caracteres
	load_char_table(char_table);
}

/**
 * \todo Desactivar o rato
 */
void winsys_finish (void) 
{
	winsys_clear ();
	winsys_textmode ();
	heap_delete (win_heap);
	heap_delete (aux_heap);
}

void winsys_textmode (void)
{
	leave_graphics (map);
}

void winsys_graphicmode (enum Resolution res)
{
	base = enter_graphics (res, &map);
	clear_screen (0, base);
	// Cria o buffer;
	dbuffer = (char *) malloc (HRES * VRES);
}

static void destroy_window (Window win);

void winsys_clear (void)
{
	Window win;

	// Enquanto existem janelas
	while ((win = heap_pop(win_heap)) != NULL) {
		destroy_window (win);
	}
}

void winsys_set_coord (int x, int y)
{
	winsys_x = x;
	winsys_y = y;
}

void winsys_get_coord (int *x, int *y)
{
	*x = winsys_x;
	*y = winsys_y;
}

void winsys_set_diff_coord (int dx, int dy) 
{
	winsys_x += dx;
	winsys_y += dy;
}

Window window_new (int x, int y, int w, int h, void *data)
{
	// Cria a janela
	Window win = (Window) malloc (sizeof (struct Window_));
	// Copia os parâmetros passados
	win->x = x;
	win->y = y;
	win->w = w;
	win->h = h;
	win->data = data;
	// Janela oculta
	win->draw = NULL;
	// Janela é estática
	win->update = NULL;
	// Inicia a janela sobre as outras
	win->plane = next_plane;
	next_plane++;
	// Os dados não liberam recursos
	win->free = NULL;
	heap_push (win_heap, win);
	return win;
}

/** Destrói uma janela sem alterar o heap */
static void destroy_window (Window win)
{
	if (win->free != NULL) {
		win->free (win->data);
	}
	free (win);
}

void window_delete (Window win)
{
	// Libera os recursos associados à janela
	if (win->free != NULL) {
		win->free (win->data);
	}
	heap_remove (win_heap, win);
	free (win);
}

void window_get_rect (Window win, int *x, int *y, int *w, int *h)
{
	*x = win->x;
	*y = win->y;
	*w = win->w;
	*h = win->h;
}

void window_move (Window win, int x, int y)
{
	win->x = x;
	win->y = y;
}

void window_move_diff (Window win, int dx, int dy)
{
	win->x += dx;
	win->y += dy;
}

void window_show (Window win, WindowDrawFunc draw)
{
	win->draw = draw;
}

void window_hide (Window win)
{
	win->draw = NULL;
}

void window_set_plane (Window win, enum WindowPlane plane)
{
	win->plane = -plane;
	next_plane = plane + 1;
}

void window_set_free (Window win, void (*func) (void *data))
{
	win->free = free;
}

/** Transforma uma coordenada x do sistema de coordenadas da janela para o
 * sistema de coordenadas do ecrã
 *
 * \param WIN janela
 * \param X Coordenada x
 * \return A coordenada X correspondente ao ecrã
 * \warning Esta função não testa se a coordenada retornada é valida.
 */
#define SCREEN_X(WIN,X) ( (WIN)->x + (X) - winsys_x)

/** Transforma uma coordenada y do sistema de coordenadas da janela para o
 * sistema de coordenadas do ecrã
 *
 * \param WIN janela
 * \param Y Coordenada y
 * \return A coordenada Y correspondente ao ecrã
 * \warning Esta função não testa se a coordenada retornada é valida.
 */
#define SCREEN_Y(WIN,Y) ( (WIN)->y + (Y) - winsys_y)

/** Desenha um pixel no buffer
 *
 * \param X Coordenada X
 * \param Y Coordenada Y
 * \param C Cor
 * \warning Esta macro não testa se as coordenadas são válidas
 */
#define DBUF_SET_PIXEL(X, Y, C) (dbuffer[(X) + ((Y) * HRES)] = (char) (C))

/** \brief Obtém o retângulo da janela que pode ser desenhado no ecrã
 *
 * \param win Janela
 * \param x Endereço onde é armazenada a coordenada X do retângulo
 * \param y Endereço onde é armazenada a coordenada Y do retângulo
 * \param w Endereço onde é armazenada a largura do retângulo
 * \param h Endereço onde é armazenada a altura do retângulo
 * \return 0 se nenhuma parte da janela está no ecrã, 1 se alguma parte da janela pode ser desenhada
 */
int get_drawable_rect (Window win, int *x, int *y, int *w, int *h) 
{
	int s_x = SCREEN_X (win, 0);
	int s_y = SCREEN_Y (win, 0);
	// Não desenha a janela se ela estiver fora do ecrã
	if (s_x >= HRES || s_y >= HRES || s_x + win->w < 0 || s_y +win->h < 0) {
		return 0;
	}
	// Calcula quanto da janela vai ser desenhado
	*w = s_x + win->w >= HRES ? HRES - s_x : win->w;
	*h = s_y + win->h >= VRES ? VRES - s_y : win->h;
	if (s_x < 0) {
		*w += s_x;
		*x = -s_x;
	} else {
		*x = 0;
	}
	if (s_y < 0) {
		*h += s_y;
		*y = -s_y;
	} else {
		*y = 0;
	}
	return 1;
}

/** \brief Macro que obtém o retângulo da janela a ser desenhado
 *
 * \param WIN Janela
 * \param X Variável onde é armazenada a coordenada X do retângulo
 * \param Y Variável onde é armazenada a coordenada Y do retângulo
 * \param W Variável onde é armazenada a largura do retângulo
 * \param H Variável onde é armazenada a altura do retângulo
 *
 * \warning Só deve ser chamado se alguma parte da janela estiver no ecrã
 * \todo Alterar se get_drawable_rect() for memoizado
 */
#define GET_DRECT(WIN,X,Y,W,H) get_drawable_rect((WIN), &(X), &(Y), &(W), &(H))

/* Preenche a janela com uma cor */
void window_fill (Window win, char c)
{
	int x, y, w, h; // Retângulo
	// Retângulo a ser desenhado:
	GET_DRECT (win, x, y, w, h);
	int s_x = SCREEN_X (win, x);
	int s_y = SCREEN_Y (win, y);
	int i, j; // Índices no loop
	for (i = 0; i < w; i++) {
		for (j = 0; j < h; j++) {
			DBUF_SET_PIXEL(s_x + i, s_y + j, c);
		}
	}
}

/* Desenha uma imagem na janela * /
void window_image (Window win, const char *img_data, int img_w, int img_h, char transp)
{
	int x, y, w, h; // Retângulo
	// Retângulo a ser desenhado:
	GET_DRECT (win, x, y, w, h);
	int s_x = SCREEN_X (win, x);
	int s_y = SCREEN_Y (win, y);
	int i, j; // Índices no loop
	// para pontos dentro do retângulo a ser desenhado e dentro da imagem...
	for (i = 0; i < w && x + i < img_w; i++) {
		for (j = 0; j < h && y + j < img_h; j++) {
			// Pixel a ser desenhado
			char c = img_data[x + i + ((y + j) * img_w)];
			// Se o ponto a ser desenhado não é transparente
			if (c != transp) {
				DBUF_SET_PIXEL(s_x + i, s_y + j, c);
			}
		}
	}
}*/

void window_image_xy (Window win,
			int x, int y,
			const unsigned char *img_data, int img_w, int img_h, int transp)
{
	int wx, wy, ww, wh; // Retângulo
	// Retângulo a ser desenhado:
	GET_DRECT (win, wx, wy, ww, wh);

	int s_x = SCREEN_X (win, x);
	int s_y = SCREEN_Y (win, y);


	int i, j; // Índices no loop
	// para pontos dentro do retângulo a ser desenhado e dentro da imagem...
	for (i = (s_x >=0 ? 0 : -s_x); i + x < win->w && i < img_w; i++) {

		if (s_x + i >= HRES) break;

		for (j = (s_y >=0 ? 0 : -s_y); j + y < win->h &&  j < img_h; j++) {
			if (s_y + j >= VRES) break;

			// Pixel a ser desenhado
			char c = (char) img_data[ i + j * img_w];
			// Se o ponto a ser desenhado não é transparente
			if (c != transp) {
				DBUF_SET_PIXEL(s_x + i, s_y + j, c);
			}
		}
	}
}


void window_fill_image (Window win, const unsigned char *img_data, char transp)
{
	window_image (win, img_data, win->w, win->h, transp);
}

/* Desenha um ponto na janela */
void window_draw_point (Window win, int x, int y, char c)
{
	// Se o ponto está dentro da janela
	if (x < win->w && y < win->h && x >= 0 && y >= 0) {
		int s_x = SCREEN_X (win, x);
		int s_y = SCREEN_Y (win, y);
		// Se o ponto está dentro do ecrã
		if (s_x < HRES  && s_y < VRES && s_x >= 0 && s_y >= 0) {
			DBUF_SET_PIXEL(s_x, s_y, c);
		}
	}
}

/* Desenha uma linha na janela usando o algoritmo de Bresenham */
static void bresenham (Window win, int x1, int y1, int x2, int y2, char c, int steep)
{
	int slope;
	int dx, dy, incE, incNE, d, x, y;
	// Linha vertical
	if (x1 == x2) {
		for (y = y1; y <= y2; y++)
			window_draw_point (win, x1, y, c);
		return;
	}
	// Onde inverte a linha x1 > x2       
	if (x1 > x2) {
		bresenham (win, x2, y2, x1, y1, c, steep);
		return;
	}
	dx = x2 - x1;
	dy = y2 - y1;

	if (abs (dx) < abs(dy)) {
		bresenham (win, y1, x1, y2, x2, c, !steep);
		return;
	}
	
	if (dy < 0) {
		slope = -1;
		dy = -dy;
	} else {
		slope = 1;
	}
	// Constante de Bresenham
	incE = 2 * dy;
	incNE = 2 * dy - 2 * dx;
	d = 2 * dy - dx;
	y = y1;
	for (x = x1; x <= x2; x++) {
		if (steep) {
			window_draw_point (win, y, x, c);
		} else {
			window_draw_point (win, x, y, c);
		}
		if (d <= 0) {
			d += incE;
		} else {
			d += incNE;
			y += slope;
		}
	}
}

void window_draw_line (Window win, int x1, int y1, int x2, int y2, char c)
{
	bresenham (win, x1, y1, x2, y2, c, 0);
}

/* Desenha um caracter na janela */
void window_draw_char (Window win, char ch, int x, int y, char color)
{
	int s_x = SCREEN_X (win, x);
	int s_y = SCREEN_Y (win, y);

	// posição do carácter 'c' em 'table' (lida de po.dat)
	char *char_def = char_table + ch*CHR_H; 
	
	int j,k,mask; //m
   
   	// usar operadores bitwise para isolar e testar os bits
	for (j=0; j<CHR_H; j++) { // for each character row
		
		// Fora do ecrã
		if (s_y + j < 0) continue;
		if (s_y + j >= VRES) break;
		// Fora da janela
		if (y + j < 0) continue;
		if (y + j >= win->h) break;
	
			
		//for (m=0; m<char_scale; m++) {
			mask = 0x80; // left pixel of current row
			for (k=0; k<8; k++) { // for each pixel in row

				// Fora do ecrã
				if (s_x + k < 0) continue;
				if (s_x + k >= HRES) break;
				// Fora da janela
				if (x + k < 0) continue;
				if (x + k >= win->w) break;

				if (*char_def & (mask >> k)) {
					DBUF_SET_PIXEL(s_x + k, s_y + j, color);
				}
			}
		//}
		char_def++;
	}
}

/* Desenha uma string na janela */
void window_draw_string (Window win, const char *str, int x, int y, char color)
{
	int i = 0;

	while(str[i] != 0) {
		window_draw_char (win, str[i], x + 8 * i, y, color);
		i++;
	}
}

/* Desenha um inteiro na janela */
void window_draw_int (Window win, int num, int x, int y, char color)
{
	Bool neg = false;
	char str[12];
	int i = 11; // 10 == último índice de str
	str [11] = '\0';

	if ( num == 0 ) {
		str[10] = '0';
		i = 10;
	} else {
		if (num < 0) {
			num = -num;
			neg = true;
		}
		
		// Constroi a string
		do {
			i--;
			str[i] = '0'+ (num%10);
			num = num/10;
		} while (num != 0);

		if (neg)	{
			i--;
			str[i]='-';
		}
	}
	window_draw_string (win, str + i, x, y, color);
}

void window_update (Window win, WindowUpdateFunc update)
{
	win->update = update;
}

/**
 * \todo Memoizar a chamada de get_drawable_rect() se for muito custoso
 */
void winsys_draw (void) 
{
	Window win;
	Heap heap;
	
	// Apaga o que estiver no buffer
	memset (dbuffer, 0, HRES * VRES);
	while ((win = (Window) heap_pop (win_heap)) != NULL) {
		// Se a janela foi destruída
		if (win->plane < 0) { // Se a janela foi reempilhada
			win->plane = -win->plane;
			// Guarda a janela ho outro heap
			heap_push (aux_heap, win);
		} else {
			// Guarda a janela ho outro heap
			heap_push (aux_heap, win);
			int x,y,w,h; // Descrição do retângulo a ser desenhado
			// A desenha
			if (win->draw != NULL && get_drawable_rect (win, &x, &y, &w, &h)) {
				win->draw (win, x, y, w, h, win->data);
			}
		}
	}
	// Troca os heaps
	heap = win_heap;
	win_heap = aux_heap;
	aux_heap = heap;
	// Copia o buffer para a memória de vídeo
	memcpy (base, dbuffer, HRES * VRES);
}

/** Função que faz o update de uma janela */
static void heap_update_func (void *el, void *data)
{
	Window win = el;

	if (win->update != NULL) {
		win->update (win, win->data);
	}
}

void winsys_update (void)
{
	 heap_forall (win_heap, heap_update_func, NULL);
}

void window_draw_rect (Window win, int x, int y, int w, int h, char color)
{
	int wx, wy, ww, wh; // Retângulo da janela
	// Retângulo da janela a ser desenhado:
	GET_DRECT (win, wx, wy, ww, wh);

	int s_x = SCREEN_X (win, x);
	int s_y = SCREEN_Y (win, y);


	int i, j; // Índices no loop
	// para pontos dentro do retângulo a janela
	for (i = (s_x >= 0 ? 0 : -s_x); i + x < win->w && i < w; i++) {

		if (s_x + i >= HRES) break;

		for (j = (s_y >= 0 ? 0 : -s_y); j + y < win->h &&  j < h; j++) {
			if (s_y + j >= VRES) break;

			DBUF_SET_PIXEL(s_x + i, s_y + j, color);
		}
	}

}

